SwiftのKeyPathについて調べた
概要
大阪オフィスの山田です。先月、パパになりました。 今回はKeyPathについて調べたのでその内容をまとめて記しておきます。
KeyPathとは
swift4にて導入されました。公式ドキュメントによると、KeyPathを使うことで動的にプロパティにアクセスできる、と書かれています。
KeyPathの種類
公式ドキュメントによるとKeyPathには以下の種類があります。 その特性で分類しました。
クラス名 | Writable | 型の指定 |
---|---|---|
AnyKeyPath | No | なし |
PartialKeyPath | No | Root |
KeyPath | No | Root, Value |
WritableKeyPath | Yes(値型) | Root, Value |
ReferenceWritableKeyPath | Yes(参照型) | Root, Value |
列挙したクラスは上から順番に継承されています。Rootは、対象のプロパティを含むクラスを指し、Valueは対象のプロパティを指します。
動作を見てみる
Read only 例.AnyKeyPath
検証用のstructを定義し、インスタンスを生成します。
struct Cat { var name: String var age: Int func meow() -> String { return "meow" } } var cat = Cat(name: "mi-san", age: 10)
AnyKeyPath変数を宣言します。
let anyKeyPath: AnyKeyPath = \Cat.name
バックスラッシュの後に、Rootとなる型、それとプロパティを指定します。 続けて、この変数をprintで出力して観察してみます。
print("AnyKeyPath: \(anyKeyPath)") // AnyKeyPath: Swift.WritableKeyPath<__lldb_expr_3.Cat, Swift.String> print(cat[keyPath: anyKeyPath]!) // mi-san print(type(of: anyKeyPath).rootType) // Cat print(type(of: anyKeyPath).valueType) // String
anyKeyPath変数をprintすると、AnyKeyPath
ではなく、何故かWritableKeyPath
とログに出力されています。これは原因が不明でした。
rootType、valueTypeで、定義したstructの名前と値の型の名前が取得できます。
実際に、値を変更しようとすると、リードオンリーのためエラーが発生します。
cat[keyPath: anyKeyPath] = "nyaaan" // Cannot assign through subscript: 'cat' is immutable
型の指定
Rootのみ型を指定するPartialKeyPath
変数宣言時に、Rootとなる型を指定します。これにより、具体的なKeyPathを指定する際にRootとなる型を省略することが可能です。
let partialKeyPath: PartialKeyPath<Cat> = \.name
RootとValueそれぞれ型を指定する 例.KeyPath
変数宣言時に、RootとValueの型を指定します。
let keyPath: KeyPath<Cat, String> = \.name
Valueの型が違うプロパティを指定するとコンパイルエラーが発生します。
let keyPath: KeyPath<Cat, String> = \.age // Key path value type 'Int' cannot be converted to contextual type 'String'
Writable
以下のように指定するか、もしくは型を省略しても指定するKeyPathがstruct型の場合は自動でWritableKeyPathとして解釈してくれる模様です。
let writableKeyPath: WritableKeyPath<Cat, String> = \.name
or
let writableKeyPath = \Cat.name
実際に値を変更して、printしてみると、値が変わっていることがわかります
let writableKeyPath = \Cat.name print(cat.name) // mi-san print(type(of: writableKeyPath)) // WritableKeyPath<Cat, String> cat[keyPath: writableKeyPath] = "nyaaan" // 値を変更する print(cat.name) // nyaaan
ただし、このケースにおけるcat
変数はletで宣言されている場合は変更ができません。
// catをletで宣言した場合 cat[keyPath: writableKeyPath] = "nyaaan" // Cannot assign through subscript: 'cat' is a 'let' constant
WritableKeyPathは値型に適用できます。今回、structは値型なのでWritableKeyPathが適用されています。一方でReferenceWritableKeyPathは参照型に使用することが可能です。
以下のようなclassを定義します。
class Dog { var name: String var age: Int init(name: String, age: Int) { self.name = name self.age = age } func bowwow() -> String { return "bowwow" } }
定義したクラスでKeyPath変数を作成しprintしてみます。ReferenceWritableKeyPath
クラスが自動で適用されています。
var dog = Dog(name: "bi-guru", age: 15) let referenceWritableKeyPath = \Dog.name print(type(of: referenceWritableKeyPath)) // ReferenceWritableKeyPath<Dog, String>
ネストしたクラスのプロパティへアクセスする
AnyKeyPathクラスは_AppendKeyPath
プロトコルを継承しています。このプロトコルのappending
メソッドを使うことで、ネストしたクラスや構造体のプロパティへのKeyPathを作成することができます。
以下のような構造体を用意します。
struct Shop { var id: String var name: String var address: Address } struct Address { var postCode: String var address: String }
ShopはAddressをプロパティとして持っています。これらの構造体のプロパティのKeyPathを作成します。
let address = Address(postCode: "123-1234", address: "住所") let shop = Shop(id: "AX001", name: "店舗名", address: address) let addressKeyPath = \Shop.address let postCodeKeyPath = \Address.postCode let shopPostCodeKeyPath = addressKeyPath.appending(path: postCodeKeyPath) print(shop[keyPath: addressKeyPath]) print(address[keyPath: postCodeKeyPath]) //print(shop[keyPath: postCodeKeyPath]) // Shop構造体にはpostCodeプロパティが存在しないのでコンパイルエラーとなる print(shop[keyPath: shopPostCodeKeyPath]) // Shop -> address -> postCodeと辿ることができ、postCodeの値 "123-1234"が出力される
Shop構造体のaddress
プロパティと、Address構造体のpostCodeプロパティのKeyPathを作成した後、addressKeyPath
のappendingメソッドを使い、postCodeKeyPath
と連結しています。
appendingを使用して連結しましたが、\Shop.address.postCode
と、KeyPathを指定することも可能です。
Swift5.2で変更があった部分について
Swift5.2でKeyPathをファンクションのように扱うことが可能となりました。 詳細はこちらに記載されています。Swift Evolution proposal: Key Path Expressions as Functions
let cats = [ Cat(name: "mi-san", age: 8), Cat(name: "kotaro", age: 6), Cat(name: "momiji", age: 12), Cat(name: "mame", age: 2), Cat(name: "chi-", age: 13) ] print(cats.map { $0.name }) // 今までの書き方 print(cats.map(\.name)) // swift5.2で使えるようになった書き方
map
だけでなくfilter
やcompactMap
も同様の書き方ができるようになっています。
KeyPathの使いみち
いくつか使い方はあるかと思いますが一例を取り上げます。
AutoLayoutの設定に使用する
以下の記事でKeyPathを使ってAutoLayoutの設定をスマートに記述している例が記載されています。
Swift Tip: Auto Layout with Key Paths
その方法について、紹介します。
まず、2つのViewの同じタイプの制約をつけるequal
ヘルパーメソッドを定義します。
func equal<L, Axis>(_ to: KeyPath<UIView, L>) -> (UIView, UIView) -> NSLayoutConstraint where L: NSLayoutAnchor<Axis> { return { view1, view2 in view1[keyPath: to].constraint(equalTo: view2[keyPath: to]) } }
このヘルパーメソッド自体は、UIViewのNSLayoutAnchor
使い方としては以下のようになります。
let constraint = equal(\.topAnchor)(view1, view2)
view1とview2のtopAnchorをつけることができます。
関数のシグネチャをわかりやすくするために、(UIView, UIView) -> NSLayoutConstraint
をtypealiasで別名をつけるとスマートになります。
typealias Constraint = (UIView, UIView) -> NSLayoutConstraint func equal<L, Axis>(_ to: KeyPath<UIView, L>) -> Constraint where L: NSLayoutAnchor<Axis> { return { view1, view2 in view1[keyPath: to].constraint(equalTo: view2[keyPath: to]) } }
当然ながら、2つのViewで違うanchorに制約を付けたい場合もあるので以下のように別定義し、constantも指定できるようにします。元の定義は1つの引数を引き受けられるように修正します
func equal<L, Axis>(_ from: KeyPath<UIView, L>, _ to: KeyPath<UIView, L>, constant: CGFloat = 0) -> Constraint where L: NSLayoutAnchor<Axis> { return { view1, view2 in view1[keyPath: from].constraint(equalTo: view2[keyPath: to], constant: constant) } } func equal<L, Axis>(_ to: KeyPath<UIView, L>, constant: CGFloat = 0) -> Constraint where L: NSLayoutAnchor<Axis> { return equal(to, to, constant: constant) }
また、dimension anchorにも設定できるように以下の定義を追加します。
func equal<L>(_ keyPath: KeyPath<UIView, L>, constant: CGFloat) -> Constraint where L: NSLayoutDimension { return { view1, _ in view1[keyPath: keyPath].constraint(equalToConstant: constant) } }
制約を設定する際に、毎回親Viewの指定をしないですむように、UIViewの拡張としてaddSubview
メソッドを定義します。
extension UIView { func addSubview(_ other: UIView, constraint: [Constraint]) { other.translatesAutoresizingMaskIntoConstraints = false addSubview(other) addConstraints(constraint.compactMap { $0(other, self) }) } }
ここまでを経て、以下のように使用することが可能となりました。制約がスマートに設定できていると思います。
override func loadView() { let view = UIView() view.backgroundColor = .white self.view = view let view1 = UIView() view1.backgroundColor = UIColor.red view.addSubview(view1, constraint: [ equal(\.centerXAnchor), equal(\.centerYAnchor), equal(\.heightAnchor, constant: 200), equal(\.widthAnchor, constant: 200) ]) let view2 = UIView() view2.backgroundColor = UIColor.blue view1.addSubview(view2, constraint: [ equal(\.centerYAnchor, \.bottomAnchor), equal(\.leftAnchor, constant: 10), equal(\.rightAnchor, constant: -10), equal(\.heightAnchor, constant: 100) ]) }
その他の利用方法
- モデルに依存しないで、共通の項目にクラスのプロパティを設定する 上記の方法についてこちらの記事で紹介されていますのでご参照ください。 The power of key paths in Swift
おわり
寝かしつけスキルを上げたい
参考
- Key-Path Expressions: 公式リファレンス
- 【Swift】型を使うという意味を考える(Swift4で導入されたKeyPathを通して):Qiita
- Swift Tip: Auto Layout with Key Paths
- Swift 4 KeyPaths and You
- Swift Evolution proposal: Smart KeyPaths: Better Key-Value Coding for Swift
- Swift Evolution proposal: Key Path Expressions as Functions
- The power of key paths in Swift